package home.project.misc;

import haxe.io.Bytes;
import haxe.io.BytesInput;
import haxe.macro.Expr.Var;
import haxe.zip.Uncompress;
import home.project.Base64;
import home.project.Main;
import home.project.misc.Tilemap.Tileset;
import openfl.display.Graphics;
import openfl.display.Tilesheet;
import openfl.display.BitmapData;
import openfl.display.Sprite;
import openfl.geom.Point;
import openfl.geom.Rectangle;
import openfl.geom.Matrix;
import openfl.Assets;

/**
 * ...
 * @author an.vuongngocduy
 */
enum Terrain {
	None;
	Named(n:String);
}

class TerrainInfo {
	@:allow(home.project.misc.Tilemap)
	private static var __names:Array<String> = [""];
	
	public static function toInt(t:Terrain):Int {
		return switch(t) {
			case Terrain.None: -1;
			case Terrain.Named(n): __names.indexOf(n);
		}
	}
	
	public static function fromInt(i:Int):Terrain {
		if (i < 0 || i >= __names.length)
			return Terrain.None;
		return Terrain.Named(__names[i]);
	}
}

class Tileset {
	private static var __collection:Map<String, Tileset> = new Map<String, Tileset>();
	
	public var name(default, null):String;
	
	public var image(default, set):Image;
	
	public var tiler(default, null):Tilesheet;
	
	public var tileWidth(default, null):Float;
	
	public var tileHeight(default, null):Float;
		
	public var tileTerrains(default, null):Array<Array<Terrain>>;
	
	public static function create(p_name:String, p_tilew:Int, p_tileh):Tileset {
		var res:Tileset = null;
		if (!__collection.exists(p_name)) {
			res = new Tileset();
			res.name = p_name;
			res.tileWidth = p_tilew;
			res.tileHeight = p_tileh;
			res.tileTerrains = new Array<Array<Terrain>>();
			__collection.set(res.name, res);
		}
		else {
			res = __collection.get(p_name);
		}
		return res;
	}
	
	public static function get(p_name:String):Tileset {
		var res:Tileset = null;
		if (__collection.exists(p_name)) {
			res = __collection.get(p_name);
		}
		return res;
	}
	
	public function new() { }
	
	public function set_image(p_img:Image):Image {
		image = p_img;
		tiler = new Tilesheet(image.getBitmapData());
		
		var x:Float = 0;
		var y:Float = 0;
		while(y < image.height) {
			x = 0;
			while(x < image.width) {
				tiler.addTileRect(new Rectangle(x, y, tileWidth, tileHeight));
				x += tileWidth;
			}
			y += tileHeight;
		}
		return image;
	}
}

class Tileblock extends Entity {
	
	public var tileSet(default, null):String;
	
	public var tileBatch(default,null):Int;
	
	public var tileTerrains(default, null):Array<Int>;
	
	public function new(p_tileSet:String, p_bacthIdx:Int, p_width:Float, p_height:Float) {
		super();		
		isCollidable = true;	
		tileSet = p_tileSet;
		tileBatch = p_bacthIdx;
		__boundingBoxes.push(new Rect.Rect(this, 0, 0, p_width, p_width));			
	}
}

class Tilemap extends Entity {
	
	public var tileWidth(default, null):Float;
	
	public var tileHeight(default, null):Float;
	
	public var widthInTiles(default, null):Int;
	
	public var heightInTiles(default, null):Int;
	
	public var opacity(default, default):Float;
	
	private var __tileBlocks:Array<Tileblock>;
	
	private var __displayList:Map <String, Array<Float>> ;
	
	//! constructor
	public function new() {
		super();
		opacity = 1.0;
		__tileBlocks = new Array<Tileblock>();
		__displayList = new Map <String, Array<Float>> ();
	}
	
	//! create map from Tiled map editor
	public static function loadTMX(p_path:String):Tilemap {
		var map:Tilemap = new Tilemap();
		map.__children = new Array<Entity>();
		
		var root:Xml = Xml.parse(Assets.getText(p_path));
		
		var map_node:Xml = root.firstElement();
		map.widthInTiles = Std.parseInt(map_node.get("width"));
		map.heightInTiles = Std.parseInt(map_node.get("height"));
		map.tileWidth = Std.parseInt(map_node.get("tilewidth"));
		map.tileHeight = Std.parseInt(map_node.get("tileheight"));
		
		var tmxTileset:Map<Int, String> = new Map<Int, String>();
		
		//! load tilesets information from TMX file
		for (tileset_node in map_node.elementsNamed("tileset")) {
			var set:Tileset = Tileset.create(tileset_node.get("name"),
								Std.parseInt(tileset_node.get("tilewidth")),
								Std.parseInt(tileset_node.get("tileheight")));
			
			var firstGid = Std.parseInt(tileset_node.get("firstgid"));
			
			tmxTileset.set(firstGid, set.name); //! save this for later use...
			
			var image_node:Xml = tileset_node.elementsNamed("image").next();
			
			set.image = new Image(image_node.get("source"), 
								image_node.exists("trans") ? Std.parseInt("0xFF" + image_node.get("trans")) : null);
			
			var terraintypes_node:Xml = tileset_node.elementsNamed("terraintypes").next();
			
			if(terraintypes_node != null) {
				var begin:Int = TerrainInfo.__names.length;
				for (tr_node in terraintypes_node.elements()) {
					if (TerrainInfo.__names.indexOf(tr_node.get("name")) != -1)
						TerrainInfo.__names.push(Std.string(firstGid) + ":" + tr_node.get("name"));
					else
						TerrainInfo.__names.push(tr_node.get("name"));
				}

				for (tl_node in tileset_node.elementsNamed("tile")) {
					var id:Int = Std.parseInt(tl_node.get("id"));
					var tr_arr:Array<String> = tl_node.get("terrain").split(",");
					set.tileTerrains[id] = new Array<Terrain>();
					for (tr_elem in tr_arr) {
						var t:Null<Int> = Std.parseInt(tr_elem);
						set.tileTerrains[id].push(t == null ? TerrainInfo.fromInt(-1) : TerrainInfo.fromInt(begin + t));
					}
				}				
			}
		}
		
		//! Load layers into child-Tilemaps 
		for (layer_node in map_node.elementsNamed("layer")) {
			var layer:Tilemap = new Tilemap();
			layer.name = layer_node.get("name");
			layer.tileWidth = map.tileWidth;
			layer.tileHeight = map.tileHeight;
			layer.widthInTiles = Std.parseInt(layer_node.get("width")); 
			layer.heightInTiles = Std.parseInt(layer_node.get("height"));
			layer.opacity = layer_node.exists("opacity") ? Std.parseFloat(layer_node.get("opacity")) : 1.0;
			
			var data_node = layer_node.elementsNamed("data").next();
			
			var indices:Array<Int> = new Array<Int>();
			
			if (data_node.exists("encoding")) {
				var encoder:String = data_node.get("encoding");
				if (encoder == "base64") {
					var decoded:Bytes = Base64.decode(data_node.firstChild().nodeValue);
					if (data_node.exists("compression")) {
						var compress:String = data_node.get("compression");
						if (compress == "zlib") {
							var stream:BytesInput = new BytesInput(Uncompress.run(decoded));
							stream.bigEndian = false;
							stream.position = 0;
							while (stream.position < stream.length) {
								indices.push(stream.readInt32());								
							}
							stream.close();
						}
					}
				}
				else if (encoder == "csv") {
					var cvs:Array<String> = data_node.firstChild().nodeValue.split(",");
					for (id in cvs){
						var gid:Null<Int> = Std.parseInt(id);
						indices.push(gid == null ? 0 : gid);
					}
				}
			}
			else {
				//not yet implemented
			}
			
			layer.__tileBlocks = new Array<Tileblock>();

			for (idx in 0...indices.length) {
				var gid:Int = indices[idx];
			
				//! choose the correct tileset
				var inTileset:String = null;
				var batchIndex:Int = -1;

				for (k in tmxTileset.keys()) {
					if (gid >= k) {
						inTileset = tmxTileset.get(k);
						batchIndex = gid - k;
					}
				}
					
				if (inTileset != null) {
					var x:Float = Std.int(idx % layer.widthInTiles) * map.tileWidth;
					var y:Float = (1 + Std.int(idx / layer.widthInTiles)) * map.tileHeight - Tileset.get(inTileset).tileHeight;
					
					layer.__tileBlocks[idx] = new Tileblock(inTileset, batchIndex, map.tileWidth, map.tileHeight);
					layer.__tileBlocks[idx].position = new Point(x, y);
				}
				else {
					layer.__tileBlocks[idx] = null;
				}
			}
			map.__children.push(layer);
		}
		
		for (objgroup_node in map_node.elementsNamed("objectgroup"))
		{
			//not implemented
		}
			
		for (imglayer_node in map_node.elementsNamed("imagelayer"))
		{
			//not implemented
		}
		
		return map;
	} 
		
	//! load collision map
	public function loadCollisions(p_csv:String):Bool {
		var collisionIndices:Array<String> = p_csv.split(",");
		var tileIdx:Int = 0;
		for (ci in collisionIndices) {
			var index:Null<Int> = Std.parseInt(ci);
			if(index > 0){
				if (__tileBlocks[tileIdx] == null){
					__tileBlocks[tileIdx] = new Tileblock(null, -1, tileWidth, tileHeight);					
					__tileBlocks[tileIdx].position = new Point(
						Std.int(tileIdx % widthInTiles) * tileWidth, 
						(1 + Std.int(tileIdx / widthInTiles)) * tileHeight - tileHeight
					);
				}
				addChild(__tileBlocks[tileIdx]);
			}
			tileIdx++;
			if (tileIdx >= widthInTiles * heightInTiles){
				break; // out of map
			}
		}
		return false;
	}
	
	public function getTerrains(c:Int, r:Int):Iterator<Array<Terrain>> {
		return null;
	}
	
	//! display the tilemap
	public override function draw(p_g:Graphics):Void {
		for (e in __children) {
			if (Std.is(e, Tilemap)) {
				var tm:Tilemap = cast(e, Tilemap);
				tm.draw(p_g);
			}
		}
		
		if(__tileBlocks != null) {
			//! build a display list
			if (__displayList == null)
				__displayList = new Map<String, Array<Float>>();
			else 
				for (e in __displayList) e.splice(0, e.length);
				
			var iiii = 0;
			for (b in __tileBlocks) {
				if (b == null) continue;
				if (b.tileSet == null || b.tileBatch < 0 || (Camera.current != null ? !Camera.current.contains(b):  false)){
					continue;
				}
				var cam_x:Float = Camera.current != null ? Camera.current.absoluteTransform.tx : 0;
				var cam_y:Float = Camera.current != null ? Camera.current.absoluteTransform.ty : 0;
				
				if (!__displayList.exists(b.tileSet))
					__displayList[b.tileSet] = new Array<Float>();
				__displayList[b.tileSet].push(b.absoluteTransform.tx - cam_x);
				__displayList[b.tileSet].push(b.absoluteTransform.ty - cam_y);
				__displayList[b.tileSet].push(b.tileBatch);
				__displayList[b.tileSet].push(opacity);
			}
			
			for (k in __displayList.keys()) {
				Tileset.get(k).tiler.drawTiles(p_g, __displayList[k], false, Tilesheet.TILE_ALPHA);
			}
		}
	}		
}